# -*- coding: UTF-8 -*-
'''
Copyright (c) 2006-2007, PreFab Software Inc.

Copyright (c) 2006, Andrew Gwozdziewycz <apgwoz@gmail.com>
All rights reserved.
Modificat per aaloy : apsl.net
15-4-2008 : Removed the anchor search in Markdown as we have it also in html.
20-7-2008 : Adapted to the new django trunk release (new-forms, new-admin)i
21-7-2008 : Added documentation about PingURL
'''


import datetime
import logging
import re
import socket
import threading
import xmlrpclib
from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.template.defaultfilters import slugify
from django.db.models.signals import post_save
from django.conf import settings
#from django.utils.safestring import mark_safe
from django.utils.html import strip_tags
from django.utils.encoding import smart_str, smart_unicode
import xml.etree.ElementTree as ET
import markdown
from blogmaker.util import expand_shortcuts, strip_domain_and_rest
from blogmaker.util.helper import Link
import blogmaker.blog.trackback_client as tc
from django.utils.translation import ugettext as _
import djapian

################################### Ping URL ###########################################

# Thanks to http://www.imalm.com/blog/2007/feb/10/using-django-signals-ping-sites-update/
# for this code!
# That is, any time you post an Entry, the blog is going to open a Thread to each of the
# definied services in PingUrl and they will inform that the blog has new entries.
# Samples:
# Technorati: http://rpc.technorati.com/rpc/ping
# Google: http://blogsearch.google.com/ping/RPC2

class PingUrl(models.Model):
    ping_url = models.URLField(verify_exists=False)
    blog_url = models.URLField(verify_exists=False)
    blog_name = models.CharField(max_length=200)

    @staticmethod
    def pingAll():

        def _pingAll():
            oldTimeout = socket.getdefaulttimeout()
            socket.setdefaulttimeout(30)
            for pingObj in PingUrl.objects.all():
                try:
                    s = xmlrpclib.Server(pingObj.ping_url)
                    reply = s.weblogUpdates.ping(pingObj.blog_name, pingObj.blog_url)
                except:
                    logging.error('Ping failed for ' + pingObj.ping_url, exc_info=True)
            socket.setdefaulttimeout(oldTimeout)
        threading.Thread(target=_pingAll).start()


################################### Tag ###########################################

class Tag(models.Model):
    tag = models.CharField(max_length=255, help_text=_("Utilitza paraules senceres, sense espais!"))

    def __unicode__(self):
        return self.tag

    def get_absolute_url(self):
        return '%stag/%s/' % (settings.BLOG_ROOT, slugify(self.tag))


################################### Entry ###########################################

class EntryManager(models.Manager):

    def current_active(self):
        ''' A QuerySet of all active entries before the current date/time '''

        # Note: The value of 'now' is cached in the returned QuerySet
        # so don't cache and reuse the QuerySet itself
        now = datetime.datetime.now()
        return self.filter(active=True).exclude(pub_date__gt=now)

    def index_blog(self):
        ''' The most recent entry object with truncated body '''
        blog_entry = self.current_active()[0]
        body = blog_entry.body
        body = body.replace('<span class="info_icon">i</span>', '')
        body = re.split('<(hr|table|blockquote|div)', body)[0]
        body = strip_tags(body)
        body = body.split()
        body = ' '.join(body[:15])
        body = "%s ..." % body.strip()
        return blog_entry.headline, body




class Entry(models.Model):

    objects = EntryManager()

    pub_date = models.DateTimeField(db_index=True)
    slug = models.SlugField(unique_for_date='pub_date',
        max_length=120, help_text=_("camp slug a partir del header"))
    headline = models.CharField(max_length=255)
    summary = models.CharField(max_length=255, null=True, blank=True, help_text=_("Deixa aquest camp buid"))
    image = models.ImageField(upload_to='photos/%Y/%m/%d', null=True, blank=True)
    copyright = models.CharField(max_length=255, null=True, blank=True,
            help_text=_("Elegeix una imatge i posa a qui pertany si escau"))
    body = models.TextField(_("cos de l'article en HTML"), null=True, blank=True)
    active = models.BooleanField(default=True, help_text=_("Es el post visible en el blog?"))
    user = models.ForeignKey(User, default=settings.DEFAULT_BLOG_USER, related_name="user_entries")
    tags = models.ManyToManyField(Tag, blank=True)
    related_entries = models.ManyToManyField("self", blank=True, symmetrical=False)
    externalId = models.IntegerField(blank=True, null=True)
    body_markdown = models.TextField("Markdown", help_text=_("Article en format markdown"))

    class Meta:
        get_latest_by = "pub_date"
        ordering = ['-pub_date']
        verbose_name = "Apunt"
        verbose_name_plural = "Apunts"

    def __unicode__(self):
        return self.headline

    def comments_closed(self):
        """Returns true if the date of the post does not allow comments."""
        days = datetime.timedelta(days=settings.BLOG_CLOSE_COMMENTS_INTERVAL)
        max_coment_date = self.pub_date + days
        return datetime.datetime.today() > max_coment_date


    def get_absolute_url(self):
        return '%s%s/%s/' % (settings.BLOG_ROOT, self.pub_date.strftime('%Y/%m/%d'), self.slug)

    def get_tags(self):
        return self.tags.all()

    def get_related_entries(self):
        return self.related_entries.all()

    anchorRe = re.compile(r'''<\s*a\s[^>]*\bhref\s*=\s*["'].*?["']\s*.*?<\s*/a\s*>''',
            re.IGNORECASE| re.MULTILINE| re.DOTALL| re.VERBOSE| re.UNICODE)
    @property
    def links(self):
        ''' Return a list of the target href of all links in this entry, as the post is going to be
        presneted as html we can use the html form '''
        links = dict()
        # <a> tags
        for anchor in self.anchorRe.findall(self.body):
            try:
                element = ET.XML(smart_str(anchor))
                links[element.get('href')]=Link(anchor=element.text, url=element.get('href'),title=element.get('title'))
            except:
                pass
        return links.values()

    @property
    def excerpt(self):
        ''' A brief excerpt for trackbacks '''
        ustr = smart_unicode(self.body, encoding="utf-8")
        ustr = strip_tags(ustr)
        return ustr[:200]

    def save(self, force_insert=False, force_update=False):
        if settings.WP_IMPORT:
            super(Entry, self).save(force_insert, force_update)
        else:
            self.body = expand_shortcuts(markdown.markdown(self.body_markdown, ['codehilite',] ))
            super(Entry, self).save(force_insert, force_update) # Call the "real" save() method.
            if settings.TRACKBACKS_ON_SAVE:
                try:
                    self.update_trackbacks()
                except:
                    logging.error("No se han actualizado los trackbacks")
            if settings.USE_PING and self.active:
                PingUrl.pingAll()
    # Don't send trackbacks to any of these sites

    _dontTrackbackSites = set('google.com technorati.com alexa.com quantcast.com'.split())

    def update_trackbacks(self):
        ''' Update the current list of trackbacks to match the current links '''
        trackbacksToDelete = dict((tb.link, tb) for tb in self.trackbacks.all())
        for link in self.links:
            if link.url in trackbacksToDelete:
                # Link has a trackback, keep it
                del trackbacksToDelete[link.url]
            else:
                host, rest = strip_domain_and_rest(link.url)

                # Skip known don't trackback sites
                if host in self._dontTrackbackSites:
                    continue
                # Skip links with no path info or just /blog/
                if rest in ('/', '/blog'):
                    continue

                # Create a new trackback for this link
                self.trackbacks.create(link=link.url)

        # Delete trackbacks that no longer have links unless they have some status
        for tb in trackbacksToDelete.values():
            if tb.status == 'NotYet':
                tb.delete()


    @staticmethod
    def entryCreateOrUpdate(pub_date, headline, body, user, externalId, active=True):
        lowerHeadline = headline.lower()
        slug = re.sub(' & ', '-and-', lowerHeadline)
        slug = re.sub(r'[\s/\?]+', '-', slug)
        slug = re.sub(r'[^\w.:~-]', '', slug)
        try:
            entry = Entry.objects.get(externalId=externalId)
        except:
            entry = Entry(externalId=externalId)
        entry.slug = slug
        entry.headline = headline
        entry.body_markdown = body
        entry.pub_date = pub_date
        entry.user = User.objects.get(id=user)
        entry.active = active
        entry.save()

class EntryIndexer(djapian.Indexer):
    fields = ['headline',
              'body_markdown',
            ]
    tags=[
        ("title", "headline", 1),
        ("body", "body_markdown", 2),
    ]
    trigger=lambda indexer, obj: obj.active

djapian.add_index(Entry, EntryIndexer, attach_as="indexer")

################################### Trackback Status ###########################################
class TrackbackStatus(models.Model):
    ''' This model holds status for trackbacks from our entries. In other words it
        represents outgoing trackbacks.
    '''
    # Note: If entry is marked edit_inline, deleting a link in an entry will not be
    # able to delete the corresponding TrackbackStatus because it will be added back
    # in by the admin save handler

    #: The entry for which the trackback was posted
    entry = models.ForeignKey(Entry, verbose_name='referring entry', related_name='trackbacks')

    #: The outside blog entry referenced from our entry
    link = models.URLField('entry link', max_length=300, verify_exists=False)

    #: The link to post the trackback to
    trackbackUrl = models.URLField('trackback link', verify_exists=False, blank=True)

    #: Datetime of most recent trackback attempt
    attempted = models.DateTimeField(null=True, blank=True)

    #: Options for status
    statusChoices = (
        ('NotYet', 'Not attempted'),
        ('NoLink', 'No trackback URL found'),
        ('Success', 'Success'),
        ('Failed', 'Failed'),
        ('Dont', 'Do not attempt'),
    )

    #: Result of most recent trackback attempt
    status = models.CharField(max_length=10, blank=False, choices=statusChoices, default='NotYet')

    @property
    def eligible(self):
        ''' Is this trackback eligible to be attempted? '''
        return (self.status=='NotYet'
            or (self.status=='Failed' and 'Throttled' in self.message)
            or (self.status=='NoLink' and self.trackbackUrl))

    #: Additional status info e.g. error message received
    message = models.TextField('additional status', blank=True)

    class Meta:
        verbose_name_plural='trackbacks'
        ordering = ('link',)

    def __unicode__(self):
        return '%s (%s)' % (self.link, self.status)


    def appendMessage(self, message):
        if self.message:
            self.message = self.message + '\n' + message
        else:
            self.message = message


    def attempt(self):
        ''' Attempt to post this trackback if it is eligible.
            Sets our status code and message.
        '''
        if not self.eligible:
            # Don't retry
            return

        self.attempted = datetime.datetime.now()
        self.message= ''
        try:
            # Get the trackback URL
            if not self.trackbackUrl:
                self.trackbackUrl, self.page_data = tc.discover(self.link)
                if self.trackbackUrl is None:
                    # Try appending /trackback/
                    self.appendMessage('Using /trackback/')
                    if self.link.endswith('/'):
                        self.trackbackUrl = self.link + 'trackback/'
                    else:
                        self.trackbackUrl = self.link + '/trackback/'

            if self.trackbackUrl:
                # Do the actual trackback
                siteName = Site.objects.get(id=settings.SITE_ID).name
                tc.postTrackback(self.trackbackUrl, self.entry.get_absolute_url(),
                        self.entry.headline, self.entry.excerpt, siteName)
                self.status = 'Success'
            else:
                self.status = 'NoLink'

        except Exception, e:
            self.status = 'Failed'
            msg = '%s: %s' % (e.__class__.__name__, e)
            # Ensure valid utf-8 with possibly some minor data loss
            msg = msg.decode('utf-8', 'replace').encode('utf-8')
            self.appendMessage(msg)

        self.save()


    @staticmethod
    def attempt_all_current():
        ''' Attempt to trackback all eligible trackback whose Entries are current. '''
        now = datetime.datetime.now()
        oneWeekAgo = now - datetime.timedelta(days=7)
        for tb in TrackbackStatus.objects.exclude(entry__pub_date__gt=now).exclude(entry__pub_date__lt=oneWeekAgo):
            tb.attempt()


# Signals
def twitt_post(sender, **kwargs):
    if settings.WP_IMPORT:
        return
    is_new = kwargs.get('created')
    if settings.SEND_TWITT_ON_POST and is_new:
        instance = kwargs.get('instance')
        import twython
        try:
            twitter = twython.core.setup(username = settings.TWITTER_USER,
                password = settings.TWITTER_PASSWORD)
            twitter.updateStatus(_(u"Nou apunt al Blog de Trespams: %s %s") %
             (instance.headline, instance.get_absolute_url(), ) )
        except twython.AuthError, e:
            logging.error("Error in twitter account")
            logging.error(e.message)
        except Exception, e:
            logging.error('Error in twitter post %s' % e.message)

def update_index(sender, **kwargs):
    "Once we have updated the post we could update the index"
    logging.info('Updating index')
    Entry.indexer.update()
    logging.info('Index updated')

# Connect the signal with the callback
post_save.connect(twitt_post, sender=Entry)
post_save.connect(update_index, sender=Entry)
